#include <string.h>
#include <stdlib.h>

#include "../core.h"
#include "../log.h"
#include "../memmap.h"
#include "fs.h"

#include "isofs.h"

const s32 sectorSize = 2048;

u8 iso_test(const char *fn)
{
    VolDescriptor desc;
    FileBlockDevice fbd = fbd_init(fn);

    if (fbd_getNumBlocks(&fbd) <= 16) {
        _log(INF, FS, "File is not an ISO: file's too small, it even can't have an header!");
        fbd_free(&fbd);
        return 0;
    }

    fbd_readBlock(&fbd, 16, (char*)&desc);

    if (memcmp(desc.cd001, "CD001", 5) != 0) {
        _log(INF, FS, "File is not an ISO: invalid header");
        fbd_free(&fbd);
        return 0;
    }

    fbd_free(&fbd);

    return 1;
}

void iso_init(ISOFileSystem *isofs, const char *filename)
{
    VolDescriptor desc;

    isofs->blockDevice = fbd_init(filename);
    isofs->nument = 0;

    fbd_readBlock(&isofs->blockDevice, 16, (char*)&desc);

    isofs->entireISO.isDirectory = 0;
    isofs->entireISO.startingPosition = 0;
    isofs->entireISO.size = fbd_getNumBlocks(&isofs->blockDevice) * fbd_getBlockSize();

    iso_readDirectory(isofs, desc.root.firstDataSectorLE, desc.root.dataLengthLE, &isofs->treeroot);

    iso_printDirectory(&isofs->treeroot, "/");
}

void iso_freedir(TreeEntry *root)
{
    u32 i;
    for (i = 0; root->children[i].size != -1; i++)
    {
        if (root->children[i].isDirectory)
            iso_freedir(&root->children[i]);
        free(root->children[i].name);
    }

    free(root->children);
    root->children = NULL;
}

void iso_free(System *fs)
{
    ISOFileSystem *isofs = fs->system;
    fbd_free(&isofs->blockDevice);

    if (isofs->treeroot.children)
        iso_freedir(&isofs->treeroot);
}

void iso_readDirectory(ISOFileSystem *isofs, u32 startsector, u32 dirsize, TreeEntry *root)
{
    u32 offset = 0;
    u32 secnum = startsector;

    char theSector[2048];

    u8 isFile;

    DirectoryEntry *dir;

    u8 nchild = 0;
    root->children = malloc(MAX_CHILD * sizeof(TreeEntry));

    fbd_readBlock(&isofs->blockDevice, secnum, theSector);

    while (secnum < (dirsize / 2048 + startsector))
    {
        dir = (DirectoryEntry *)(theSector + offset);
        
        offset += dir->size;

        if (offset >= 2048 || dir->size == 0)
        {
            offset = 0;
            secnum++;
            fbd_readBlock(&isofs->blockDevice, secnum, theSector);
            continue;
        }

        /* . or .. */
        if (dir->firstIdChar <= 1)
            continue;

        isFile = (dir->flags & 2) ? 0 : 1;

        root->children[nchild].name = malloc(strlen(&dir->firstIdChar) + 1);
        strcpy(root->children[nchild].name, &dir->firstIdChar);
        root->children[nchild].size = dir->dataLengthLE;
        root->children[nchild].startingPosition = dir->firstDataSectorLE * 2048;
        root->children[nchild].isDirectory = !isFile;

        if (!isFile)
        {
            if (dir->firstDataSectorLE == startsector)
                _log(INF, FS, "WARNING: Appear to have a recursive file system, breaking recursion");
            else
                iso_readDirectory(isofs, dir->firstDataSectorLE, dir->dataLengthLE, &root->children[nchild]);
        }

        nchild++;
    }

    root->children[nchild].size = -1;
}

void iso_printDirectory(TreeEntry *root, const char *dirname)
{
    u32 i;
    char *newdir;

    for (i = 0; root->children[i].size != -1; i++)
    {
        if (root->children[i].isDirectory) {
            _log(INF, FS, "%s%s/", dirname, root->children[i].name);
            newdir = malloc(strlen(dirname) + strlen(root->children[i].name) + 2);
            strcpy(newdir, dirname);
            strcat(newdir, root->children[i].name);
            strcat(newdir, "/");
            iso_printDirectory(&root->children[i], newdir);
            free(newdir);
        }
        else
            _log(INF, FS, "%s%s", dirname, root->children[i].name);
    }
}

TreeEntry *iso_getFromPath(ISOFileSystem *isofs, const char *path)
{
    TreeEntry *e;

    if (path[0] == '\0')
        return &isofs->entireISO;

    e = &isofs->treeroot;

    while (*(path++) != '/')
        ;

    for (;;)
    {
        TreeEntry *ne = NULL;
        char *name;

        if (path[0] != '\0')
        {
            u32 i;
            u8 size;
            for (size = 0; path[size] != '\0' && path[size] != '/'; size++)
                ;
            for (i = 0; i < MAX_CHILD && e->children[i].size != -1; i++)
            {
                name = e->children[i].name;
                if (size == strlen(name))
                {
                    if (strncmp(path, name, strlen(name)) == 0) {
                        ne = &e->children[i];
                        break;
                    }
                }
            }
        }
        if (ne)
        {
            e = ne;
            path = &path[strlen(name)];

            if ((path[0] == '\0') || ((strlen(path) == 1) && (path[0] == '/')))
                return e;

            path = &path[1];
        }
        else
        {
            _log(WRN, FS, "File %s not found", path);
            return 0;
        }
    }
}

s32 iso_getEntry(ISOFileSystem *isofs, u32 handle)
{
    u32 i;
    for (i = 0; i < isofs->nument; i++)
        if (isofs->handles[i] == handle)
            return i;
    return -1;
}

s32 iso_openFile(System *fs, const char *filename, FileAccess access)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;
    OpenFileEntry entry;
    char *realfn;
    u8 off = 0;

    while (filename[(off++) + 1] != '/' && off < strlen(filename))
        ;

    if (off == strlen(filename)) /* No / in directory: raw write from start of the disc */
    {
        entry.isRawSector = 1;
        entry.sectorStart = 0;
        entry.openSize = isofs->blockDevice.filesize;
        entry.file = NULL;
        entry.seekPos = 0;
        isofs->handles[isofs->nument] = mfs_getNewHandle();
        isofs->entries[isofs->nument] = entry;
        isofs->nument++;
        return mfs_getHandle();
    }

    realfn = malloc(strlen(filename) - off + 1);
    realfn[0] = '\0';
    strcpy(realfn, &filename[off]);

    if (strncmp(realfn, "/sce", 4) == 0)
    {
        u32 sect, size;
        char *lbn = strstr(realfn, "_lbn") + 4;
        char *siz = strstr(realfn, "_size") + 5;
        if (!lbn || !siz) {
            _log(ERR, FS, "Bad raw sector reading?");
            return 0;
        }
        sscanf(lbn, "0x%08x", &sect);
        sscanf(siz, "0x%08x", &size);
        _log(INF, FS, "Got a raw sector read: sector %08x, size %08x", sect, size);
        free(realfn);

        entry.isRawSector = 1;
        entry.sectorStart = sect;
        entry.openSize = size;
        entry.file = NULL;
        entry.seekPos = 0;
        if (isofs->nument >= MAX_ENTRIES) {
            _log(ERR, FS, "Max ISO open files limit reached!");
            core_stop();
        }
        isofs->handles[isofs->nument] = mfs_getNewHandle();
        isofs->entries[isofs->nument] = entry;
        isofs->nument++;
        return mfs_getHandle();
    }
    free(realfn);

    entry.isRawSector = 0;

    if (access & FILEACCESS_WRITE) {
        _log(INF, FS, "Can't open file %s with write access on an ISO partition", filename);
        return 0;
    }

    entry.file = iso_getFromPath(isofs, filename);
    if (!entry.file)
        return 0;

    entry.seekPos = 0;
    entry.openSize = 0;
    entry.sectorStart = 0;

    isofs->handles[isofs->nument] = mfs_getNewHandle();
    isofs->entries[isofs->nument] = entry;
    isofs->nument++;
    return mfs_getHandle();
}

void iso_closeFile(System *fs, u32 handle)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;

    s32 entry = iso_getEntry(isofs, handle);
    if (entry != -1)
    {
        u32 i;
        isofs->nument--;
        for (i = entry; i < isofs->nument; i++) {
            isofs->entries[i] = isofs->entries[i + 1];
            isofs->handles[i] = isofs->handles[i + 1];
        }
    }
    else {
        /* This shouldn't happen... */
        _log(INF, FS, "Hey, what are you doing? Closing non-open files?");
    }
}

s32 iso_ownsHandle(System *fs, u32 handle)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;
    u32 i;
    for (i = 0; i < isofs->nument; i++)
        if (isofs->handles[i] == handle)
            return 1;
    return 0;
}

u32 iso_seekFile(System *fs, u32 handle, s32 position, FileMove type)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;
    s32 entry = iso_getEntry(isofs, handle);

    if (entry != -1)
    {
        OpenFileEntry *e = &isofs->entries[entry];
        switch (type)
        {
        case FILEMOVE_BEGIN:
            e->seekPos = position;
            break;

        case FILEMOVE_CURRENT:
            e->seekPos += position;
            break;

        case FILEMOVE_END:
            if (e->isRawSector)
                e->seekPos = e->openSize;
            else
                e->seekPos = e->file->size - position;
            break;
        }
        return e->seekPos;
    }

    _log(WRN, FS, "Hey, what are you doing? Seeking in non-open files?");
    return 0;
}

size_t iso_readFile(System *fs, u32 handle, char *pointer, u32 size)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;
    s32 entry = iso_getEntry(isofs, handle);

    if (entry != -1)
    {
        OpenFileEntry *e = &isofs->entries[entry];
        u32 position;
        s32 totalRead, secNum, posInSector, remain;
        char theSector[2048];
        if (e->isRawSector)
        {
            position = e->sectorStart;
            if (position + size > e->sectorStart + e->openSize)
                size = e->sectorStart + e->openSize - position;
            position *= 2048;
        }
        else
        {
            if (e->seekPos > e->file->size - size)
                size = e->file->size - e->seekPos;
            position = e->file->startingPosition + e->seekPos;
        }

        totalRead = 0;
        secNum = position / 2048;
        posInSector = position & 2047;
        remain = size;        

        while (remain > 0)
        {
            s32 bytesToCopy = 2048 - posInSector;
            fbd_readBlock(&isofs->blockDevice, secNum, theSector);
            if (bytesToCopy > remain)
                bytesToCopy = remain;

            memcpy(pointer, theSector + posInSector, bytesToCopy);
            totalRead += bytesToCopy;
            pointer += bytesToCopy;
            remain -= bytesToCopy;
            posInSector = 0;
            secNum++;
        }
        e->seekPos += size;
        return totalRead;
    }

    _log(WRN, FS, "Hey, what are you doing? Reading non-open files?");
    return 0;
}

size_t iso_readFileToAddr(System *fs, u32 handle, u32 addr, u32 size)
{
    ISOFileSystem *isofs = (ISOFileSystem*)fs->system;
    s32 entry = iso_getEntry(isofs, handle);

    if (entry != -1)
    {
        OpenFileEntry *e = &isofs->entries[entry];
        u32 position;
        u32 totalRead, secNum, posInSector, remain;
        char theSector[2048];
        if (e->isRawSector)
        {
            position = e->sectorStart;
            if (position + size > e->sectorStart + e->openSize)
                size = e->sectorStart + e->openSize - position;
        }
        else
        {
            if (e->seekPos > e->file->size - size)
                size = e->file->size - e->seekPos;
            position = e->file->startingPosition + e->seekPos;
        }

        totalRead = 0;
        secNum = position / 2048;
        posInSector = position & 2047;
        remain = size;        

        while (remain > 0)
        {
            u32 bytesToCopy = 2048 - posInSector;
            u32 i;
            char *curPos = theSector + posInSector;
            fbd_readBlock(&isofs->blockDevice, secNum, theSector);
            if (bytesToCopy > remain)
                bytesToCopy = remain;

            for (i = 0; i < bytesToCopy; i++)
                mem_write8(addr + i, (u8)curPos[i]);

            totalRead += bytesToCopy;
            addr += bytesToCopy;
            remain -= bytesToCopy;
            posInSector = 0;
            secNum++;
        }
        e->seekPos += size;
        return totalRead;
    }

    _log(WRN, FS, "Hey, what are you doing? Reading non-open files?");
    return 0;
}

FileInfo iso_getFileInfo(System *fsc, const char *filename) 
{
    ISOFileSystem *fs = (ISOFileSystem*)fsc->system;
    TreeEntry *entry = iso_getFromPath(fs, filename);
    FileInfo x; 
    if (!entry)
        x.size = 0;
    else
    {
        strcpy(x.name, entry->name);
        x.access = FILEACCESS_READ;
        x.size = entry->size; 
        x.type = entry->isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
        x.isOnSectorSystem = 1;
        x.startSector = entry->startingPosition / 2048;
    }
    return x;
}

u32 iso_getDirListing(System *fsc, const char *path, FileInfo **listing)
{
    ISOFileSystem *fs = fsc->system;
    u32 i;
    u32 nument;
    TreeEntry *entry = iso_getFromPath(fs, path);
    if (!entry)
        return -1;

    for (i = 0; i < MAX_CHILD && entry->children[i].size != -1; i++)
        ;

    nument = i;
    *listing = malloc(sizeof(FileInfo) * nument);

    for (i = 0; i < MAX_CHILD && entry->children[i].size != -1; i++)
    {
        TreeEntry *e = &entry->children[i];
        strcpy((*listing)[i].name, e->name);
        (*listing)[i].access = FILEACCESS_READ;
        (*listing)[i].size = e->size;
        (*listing)[i].type = e->isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
    }

    return i;
}

void iso_freeDirListing(FileInfo *listing)
{
    free(listing);
}

